/*
 * Copyright 2007 Matt Jensen
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations under the License.
 */
package org.jtell.internal;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jtell.EventSink;
import org.jtell.EventSinkSource;
import org.jtell.config.EventSinkMetadata;

import java.lang.reflect.Method;
import java.util.HashSet;
import java.util.Set;

/**
 * <p>
 * <code>DefaultEventSinkSource</code> is the default implementation of the {@link EventSinkSource} interface. It
 * creates {@link EventSink} objects which interact with the event sink methods described in the supplied event sink
 * metadata.
 * </p>
 * <p>
 * <strong>Thread Safety</strong><br/>
 * Instances of this class are safe for multithreaded access. 
 * </p>
 */
public class DefaultEventSinkSource implements EventSinkSource
{
    private static Log LOG = LogFactory.getLog(DefaultEventSinkSource.class);

    /**
     * <p>
     * Construct a {@link DefaultEventSinkSource} instance.
     * </p>
     */
    public DefaultEventSinkSource()
    {
        super();
    }

    /* inherit javadoc */
    public Set<EventSink> getEventSinks(final Object listener, final Set<EventSinkMetadata> metadata)
    {
        Set<EventSink> result = Empties.EMPTY_EVENT_SINKS;
        if (!metadata.isEmpty())
        {
            // Next create the adapters.
            result = new HashSet<EventSink>(metadata.size());
            for (final EventSinkMetadata nextMetadata : metadata)
            {
                result.add(createEventSink(listener, nextMetadata));
            }
            if (LOG.isDebugEnabled())
            {
                LOG.debug(String.format("Created %d adapter(s) for listener [%s].", result.size(), listener));
            }
        }
        return result;
    }

    /**
     * <p>
     * Create an event sink object which represents the event sink on an application listener object which is described
     * by a given metadata object.
     * </p>
     *
     * @param listener the application listener object.
     * @param metadata the event sink metadata.
     * @return {@link EventSink} instance.
     */
    private final EventSink createEventSink(final Object listener, final EventSinkMetadata metadata)
    {
        // Get the method and examine its signature as appropriate.
        final Method method = ReflectionUtils.findMethodBySignature(listener.getClass(), metadata.getMethodSignature());
        final Class<?>[] parameterTypes = method.getParameterTypes();

        // Find the event parameter, if any.
        final int eventParameterIndex;
        if (0 == parameterTypes.length)
        {
            // No event parameter.
            eventParameterIndex = -1;
        }
        else if (1 == parameterTypes.length)
        {
            // Single parameter must be the event parameter. Verify assignability.
            final Class<?> eventClass;
            try
            {
                eventClass = Class.forName(metadata.getEventClassName());
            }
            catch (ClassNotFoundException e)
            {
                // Should be impossible at this point.
                throw new IllegalStateException(e);
            }
            if (!parameterTypes[0].isAssignableFrom(eventClass))
            {
                throw new IllegalArgumentException(String.format(
                        "First parameter for method [%s] on listener [%s] is of class [%s], which is not assignable to event class [%s].",
                        method, listener, parameterTypes[0], eventClass));
            }
            eventParameterIndex = 0;
        }
        else
        {
            // We support zero or one parameters.
            throw new IllegalArgumentException(String.format("Method [%s] on listener [%s] takes too many parameters.",
                    method, listener));
        }

        final EventSink result = new MethodInvokingEventSink(method, listener, metadata, eventParameterIndex);
        if (LOG.isDebugEnabled())
        {
            LOG.debug(String.format("Returning event sink adapter [%s] for method [%s] on listener [%s].", result,
                    method, listener));
        }
        return result;
    }
}
